;;; guile-hkdf --- HKDF: HMAC based Extract-and-Expand Key Derivation Function
;;; Copyright © 2020 Brendan Tildesley <mail@brendan.scot>
;;;
;;; This file is part of guile-hkdf.
;;;
;;; guile-hkdf is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or
;;; (at your option) any later version.
;;;
;;; guile-hkdf is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;;; General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with guile-gcrypt.  If not, see <http://www.gnu.org/licenses/>.

(define-module (hkdf)
  #:use-module (srfi srfi-1)
  #:use-module (gcrypt hash)
  #:use-module (gcrypt mac)
  #:use-module (rnrs bytevectors)
  #:use-module (guix base16)
  #:export (hkdf
            hkdf-expand
            hkdf-extract))
;;;; Implements https://tools.ietf.org/html/rfc5869

(define (bytevector-append . bytevectors)
  (let ((output
         (make-bytevector
          (apply + (map bytevector-length bytevectors)))))
    (let loop ((point 0)
               (bytevectors bytevectors))
      (if (null? bytevectors)
          output
          (let ((bv (car bytevectors)))
            (bytevector-copy! bv
                              0
                              output
                              point
                              (bytevector-length bv))
            (loop (+ point (bytevector-length bv))
                  (cdr bytevectors)))))))

(define default-hmac (lookup-mac-algorithm 'hmac-sha512))
(define-syntax hmac (identifier-syntax sign-data))

(define* (hkdf-extract
          input-keying-material
          #:key
          (salt #f) ;; or 32????
          (algorithm default-hmac))
  "Calculate HKDF-extract as defined in RFC 5869. This is the first step in the
two step HKDF function. It is only intended to be optional if the
INPUT-KEYING-MATERIAL is already known to be secure, random, unbiased data."
  (hmac (or salt (make-bytevector (mac-size algorithm)))
        input-keying-material
        #:algorithm algorithm))

(define* (hkdf-expand pseudo-random-key  ;; Typically the output of hkdf-extract
                      #:key
                      (info #vu8())  ;; data from the application.
                      (length #f)
                      (algorithm default-hmac))
  "Calculate HKDF-Expand as defined in RFC 5869. This is the second step in the
HKDF function."

  (let* ((length (or length (mac-size algorithm)))
         (hash-length (mac-size algorithm))
         (blocks-needed (ceiling-quotient length hash-length)))

    (when (> length (* 255 hash-length))
      (error
       (string-append
        "We cannot output keying material that large with the chosen
MAC. Consider an algorithm with a larger digest output." )))

    ;; Initialise a bytevector with zeroes of the requested length.
    (let ((output (make-bytevector length)))
      ;; Bytevectors cannot be extended like a list, so we allocate one first.

      ;; Next we generate a series of hashes through iteration by hashing the
      ;; the previous hash before it, the information provided by the
      ;; application using this algorithm, such as a password, random data, an
      ;; arbitrary salt defined the the programs protocol for the sake of adding
      ;; a bit more uniqueness. the output is truncated to the specified length
      (let loop ((intermediate-output #vu8())
                 (counter 0))

        (if (= counter blocks-needed)
            output
            (let ((new-block
                   (hmac pseudo-random-key
                         (bytevector-append
                          intermediate-output
                          info
                          (make-bytevector 1 (+ counter 1)))
                         #:algorithm algorithm)))
              (bytevector-copy! new-block
                                0
                                output
                                (* counter hash-length)

                                ;; Append all of the new hash, or until we
                                ;; have reached the required output length.
                                (if (> hash-length (- length (* counter hash-length)))
                                    ;; We will terminte at the start of the next loop.
                                    (- length (* counter hash-length))
                                    hash-length))
              (loop new-block
                    (+ 1 counter))))))))

(define* (hkdf input-keying-material
               #:key
               length
               (info #vu8())
               (salt #vu8())
               (algorithm default-hmac))
  "Given INPUT-KEYING-MATERIAL, compute HKDF as defined in RCF 5869, outputting
LENGTH (defaults to the length of the chosen MAC algorithm) of keying material
as a BYTEVECTOR useful for various cryptographic protocols.

The KEY INFO defines non-secret application data unique to the particular use
case of the function, such as the protocol version. By default it is the empty
bytevector #vu8().

The KEY SALT is for adding optional, non-secret or pseudo-random data to be
hashed. By default, it will be a zeroed bytevector of LENGTH.

The KEY ALGORITHM (Default SHA512) specifies the HMAC algorithm, represented as
a number defined in the (gcrypt mac) module. For example, (lookup-mac-algorithm
'hmac-sha256) can be used to specify form a human readable symbol. See
https://notabug.org/cwebber/guile-gcrypt/src/master/gcrypt/mac.scm for the full
list of HMACS."

  (hkdf-expand (hkdf-extract input-keying-material
                             #:salt salt
                             #:algorithm algorithm)
               #:info info
               #:length length
               #:algorithm algorithm))
